feat(pkg-intel): add package_changelog (pkg changelog + MCP) - #18
Merged
Conversation
jlitola
force-pushed
the
feat/pkg-intel-changelog
branch
2 times, most recently
from
April 21, 2026 05:37
63ca06a to
8118922
Compare
Fourth Wave 1 package-intelligence tool, following `package_summary` (#13), `package_vulnerabilities` (#14), `package_dependencies` (#16 + follow-up #17). Behind the existing `code_navigation` capability gate. Both surfaces share a single request builder, envelope builder, and error classifier; a parity test asserts `toEqual` JSON across the two surfaces for every service-sourced fixture. ## Surfaces - **`githits pkg changelog [spec]`** — default latest-mode output is a summary row (identity · source · mode · entry count) plus, per entry, a `version date url` header followed by the first 10 lines of the markdown body (with a `… (+N more lines — use --verbose for the full body)` footer when the body is longer). `--verbose` uncaps the body preview; `--no-body` drops bodies entirely from both terminal and `--json`. `--from <v>` switches to range mode (all entries between `--from` and `--to`/latest; `--limit` rejected). `--to <v>` / `--limit <n>` shape latest mode. `--git-ref` targets a branch/tag for CHANGELOG.md source. `--repo-url <url>` is an alternative addressing mode, mutually exclusive with `<spec>`. - **`package_changelog`** — MCP tool with the same envelope. Dual addressing: `registry` + `package_name` XOR `repo_url`. Permissive Zod schema + in-handler validation. `include_bodies` (default true) mirrors CLI's `--no-body` and controls JSON envelope bodies. ## Design choices - **Dual addressing is unique to this tool.** P1 / P2 / P3 all accept only `registry` + `package_name` because their underlying backend queries are registry-metadata APIs. `packageChangelog` is intrinsically repo-level (sources: GitHub Releases, CHANGELOG.md, HexDocs), so `repoUrl` is a peer addressing mode in the GraphQL signature. Exposing it on MCP was non-negotiable: `packageSummary` cannot resolve repo-URL → spec, so agents starting from a repo URL had no path in. Documented in `tools.md` so future tool authors don't cargo-cult the asymmetry. - **`<spec>@<version>` rejected.** `pkg vulns` / `pkg deps` treat `@version` as "for this exact version"; changelog has no single-version query. Silently remapping to `toVersion` would be a client-invented semantic shift. Rejected with `INVALID_ARGUMENT` and a hint pointing to `--to` / `--from`. - **Body preview capped at 10 lines by default.** Release bodies routinely run 50-100+ lines — showing them unbounded would swamp the terminal; showing none would make the whole command useless for answering "what changed". 10-line cap shows the first one or two sections plus preamble, which is usually enough to answer the question; `--verbose` lifts the cap when full context is needed; `--no-body` drops bodies entirely for a pure timeline view. JSON envelope is always full-body unless `--no-body` / `include_bodies: false` is set — `--verbose` is terminal-only. - **Data-first envelope.** `{registry|repoUrl, source, mode, entries: {count, items}, filter?}`. `source` always present here (null-source case promoted to `NOT_FOUND` at the service boundary; envelope builder has a defence-in-depth throw if the invariant is ever violated upstream). `mode` derived from request — `"range"` iff `fromVersion` non-null, `"latest"` otherwise. `entries.count` computed client-side from `items.length`; backend count not selected on the wire. - **`version` kept when null, other per-entry nullables stripped.** `version` is the primary key agents index by. Empty-string values (e.g. `body: ""`) are preserved — distinct from absent — so agents can tell "empty release notes" from "no notes field". Terminal formatter renders `(empty release notes)` sentinel for empty-string bodies. - **`filter.*` tracks explicit fields only.** Builder returns an `explicitFilterFields` set; envelope emits `filter.*` only for caller-supplied inputs. Backend defaults (`limit: 10`, `toVersion: latest`) never round-trip as caller intent. - **`include_bodies` lever.** Default true. `false` drops `body` from every item explicitly; other fields preserved so agents still get the version / date / URL timeline. Measured 5.13× envelope size reduction on a 20-entry `npm:typescript` request (17.3 KB → 3.4 KB). - **`--no-body` + `--verbose` is an error.** Contradictory intents; CLI rejects with an actionable hint rather than silently ignoring `--verbose`. - **MCP `limit` validation in the shared builder, not Zod.** If Zod hard-rejected `limit: 51` at the SDK level, the agent would see a raw SDK error instead of our shared `{error, code, retryable}` envelope. Schema is permissive; builder enforces bounds. - **Registry coverage — all 9 on the wire, no client-side restriction.** `packageChangelog` is source-pull, not registry-query. Live-smoke confirmed npm / pypi / hex / crates / vcpkg / maven / packagist return useful data; nuget returns a generic `BACKEND_ERROR`; zig packages weren't in the backend index. All paths produce the shared envelope correctly. - **Mode mutex enforced client-side.** `--from` / `from_version` + `--limit` / `limit` → `INVALID_ARGUMENT` with actionable hint. - **`metadata` dropped in v1.** Source-specific opaque `GenericJSON`; revisit via agent feedback. - **Shared `promoteGenericVersionNotFound` extended.** Now recognises `fromVersion` / `toVersion` in addition to `version`. `registry` / `packageName` made optional so repo-URL-addressed requests flow through. Preference order `version → fromVersion → toVersion`. P2 / P3 regressions guarded by existing tests + 5 new helper tests. ## Tests - `bun test` — 1062 pass, 0 fail. - `bun run typecheck` / `bun run build` / `bun run lint` — clean. - Request builder, envelope builder, terminal formatter, MCP tool, CLI action, parity fixtures (12) — see docs for full matrix. - Live-smoke against production pkgseer: latest / range / repo-URL / `--no-body` / error paths / registry matrix / body-preview cap / `--verbose` uncap / truncation footer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jlitola
force-pushed
the
feat/pkg-intel-changelog
branch
from
April 21, 2026 06:22
8118922 to
845bddf
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fourth Wave 1 package-intelligence tool, following
package_summary(#13),package_vulnerabilities(#14), andpackage_dependencies(#16 + follow-up #17). Behind the existingcode_navigationcapability gate.githits pkg changelog [spec]— default latest-mode output is a summary row (identity · source · mode · count) plus, per entry, aversion date urlheader followed by the first 10 lines of the markdown body, indented and dimmed. Bodies longer than the cap show a… (+N more lines — use --verbose for the full body)footer.--verboseuncaps the body preview;--no-bodydrops bodies entirely from both terminal and--json.--from <v>switches to range mode (all entries between--fromand--to/latest;--limitrejected).--to <v>/--limit <n>shape latest mode.--git-reftargets a branch/tag for CHANGELOG.md source.--repo-url <url>is an alternative addressing mode, mutually exclusive with<spec>.package_changelog— MCP tool with the same envelope. Dual addressing:registry+package_nameXORrepo_url. Permissive Zod schema + in-handler validation.include_bodies(default true) mirrors CLI's--no-bodyand controls JSON envelope bodies.Design choices
registry+package_name.packageChangelogis intrinsically repo-level (sources: GitHub Releases, CHANGELOG.md, HexDocs), sorepoUrlis a peer addressing mode in the GraphQL signature. Exposing it on MCP was non-negotiable:packageSummarycannot resolve repo-URL → spec, so agents starting from a repo URL had no path in. Documented intools.mdso future tool authors don't cargo-cult the asymmetry.<spec>@<version>rejected.pkg vulns/pkg depstreat@versionas "for this exact version"; changelog has no single-version query. Silently remapping totoVersionwould be a client-invented semantic shift. Rejected withINVALID_ARGUMENTand a hint pointing to--to/--from.--verboselifts the cap when full context is needed;--no-bodydrops bodies entirely. JSON envelope is always full-body unlessinclude_bodies: false—--verboseis terminal-only.{registry|repoUrl, source, mode, entries: {count, items}, filter?}.sourcealways present (null-source case promoted toNOT_FOUNDat the service boundary; envelope builder has a defence-in-depth throw if the invariant is ever violated upstream).modederived from request —"range"ifffromVersionnon-null,"latest"otherwise.entries.countcomputed client-side fromitems.length; backend count not selected on the wire.versionkept when null, other per-entry nullables stripped.versionis the primary key agents index by. Empty-string values (e.g.body: "") are preserved — distinct from absent — so agents can tell "empty release notes" from "no notes field". Terminal formatter renders(empty release notes)sentinel for empty-string bodies under--verbose.filter.*tracks explicit fields only. Builder returns anexplicitFilterFieldsset; envelope emitsfilter.*only for caller-supplied inputs. Backend defaults (limit: 10,toVersion: latest) never round-trip as caller intent.include_bodieslever. Default true.falsedropsbodyfrom every item explicitly; other fields preserved so agents still get the version / date / URL timeline. Measured 5.13× envelope size reduction on a 20-entrynpm:typescriptrequest (17.3 KB → 3.4 KB).--no-body+--verboseis an error. Contradictory intents; CLI rejects with an actionable hint.limitvalidation in the shared builder, not Zod. If Zod hard-rejectedlimit: 51at the SDK level, agents would see a raw SDK error instead of our shared{error, code, retryable}envelope. Schema is permissive; builder enforces bounds.packageChangelogis source-pull, not registry-query. Live-smoke confirmed the backend returns useful data across npm / pypi / hex / crates / vcpkg / maven / packagist; nuget returns a genericBACKEND_ERROR; zig packages weren't indexed. All paths surface the shared envelope correctly.--from/from_version+--limit/limit→INVALID_ARGUMENTwith actionable hint.metadatadropped in v1. Source-specific opaqueGenericJSON; revisit via agent feedback.promoteGenericVersionNotFoundextended. Now recognisesfromVersion/toVersionin addition toversion.registry/packageNamemade optional so repo-URL-addressed requests flow through. Preference orderversion → fromVersion → toVersion. P2 / P3 regressions guarded by existing tests + 5 new helper tests.Tests
bun test— 1062 pass, 0 fail.bun run typecheck/bun run build/bun run lint— clean.<spec>@<version>rejection,--from/--limitmutex, limit bounds, tag-style rejection, pre-release versions on--from/--to, whitespace handling, explicit-flag tracking.include_bodieslever, mode derivation, filter echo, empty entries, defence-in-depth source-null throw.--verboseuncap,(empty release notes)sentinel, singular/plural footer wording, missing fields, range mode label.include_bodies, validation errors (incl. out-of-rangelimitvia envelope not SDK), service errors.--no-body+--verboserejection, flag interactions, truncation footer on long bodies,--verboseuncap.--no-body, default bodies, empty entries,NOT_FOUND,TargetNotFoundError,VERSION_NOT_FOUND,BACKEND_ERROR,INVALID_ARGUMENT.Live-smoke (production pkgseer)
+62 more lines — use --verbose for the full body.5.2.0, 5.0.1, 5.1.0— filed as backend gap feat(code-nav): ship capability-gated search_symbols (CLI + MCP) #10 for the pkgseer team).--to/--from. Exit code 1.BACKEND_ERROR(same pattern as P2/P3; backend doesn't emit structuredPACKAGE_NOT_FOUNDextension).BACKEND_ERROR, zig (zig-clap) returnsPackage not found(indexing gap).--no-bodypayload size:npm:typescript --limit 20→ 17.3 KB with bodies, 3.4 KB without = 5.13× smaller.Test plan
bun testpasses (1062 / 0)bun run typecheckcleanbun run buildcleanbun run lintclean (no new warnings)--no-body, payload size delta, tag-style rejection,@spec@versionrejection, body-preview cap + footer,--verboseuncap🤖 Generated with Claude Code